
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stddef.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <libgen.h>
#include <linux/limits.h>
#include <sys/wait.h>

#include "fcache_config.h"

extern char **__environ;

#define IRGEN_FILEIO_PRELOAD "IRGEN_FILEIO_PRELOAD"
#define IRGEN_CACHE_PATH "IRGEN_CACHE_PATH"
#define IRGEN_FILECACHE_SCRIPT "IRGEN_FILECACHE_SCRIPT"
#define IRGEN_PATH_MIN_MATCH_PREFIX "IRGEN_PATH_MIN_MATCH_PREFIX"

// only for linux
#define FIELDESCRIPTORDIR "/proc/self/fd"
static size_t const fdpath_len = strlen(FIELDESCRIPTORDIR);

#define false 0
#define true 1

#define DLSYM(TYPE_, VAR_, SYMBOL_)\
    union{\
        void *from;\
        TYPE_ to;\
    }cast;\
    if (0 == (cast.from = dlsym(RTLD_NEXT, SYMBOL_))){\
        perror("[ERROR] DLSYM Function %s Failed");\
        exit(EXIT_FAILURE);\
    }\
    TYPE_ const VAR_ = cast.to;

#define LOG_ERROR (0)
#define LOG_WARN  (1)
#define LOG_INFO  (2)
#define LOG_DEBUG (3)

static char const *LOG_FILE_NAME = "/tmp/fcache.log";
static FILE *LOG_FILE = NULL;

#define LOG(log_level, ...) do{\
        if (log_level <= LOG_DEBUG){\
            typedef FILE *(*libc_fopen)(const char *restrict, const char *restrict);\
            DLSYM(libc_fopen, raw_fopen, "fopen");\
            LOG_FILE = raw_fopen(LOG_FILE_NAME, "a");\
            if (!LOG_FILE){\
                perror("[WARN] Open Log File Failed");\
            }else{\
                fprintf(LOG_FILE, "%s:%d:\t", __FILE__, __LINE__);\
                fprintf(LOG_FILE, __VA_ARGS__);\
                fprintf(LOG_FILE, "\n");\
                fflush(LOG_FILE);\
                fclose(LOG_FILE);\
            }\
        }\
    }while(0);

enum{
    IRGEN_FILEIO_PRELOAD_I = 0,
    IRGEN_CACHE_PATH_I,
    IRGEN_FILECACHE_SCRIPT_I,
    IRGEN_PATH_MIN_MATCH_PREFIX_I,
    ENV_SIZE
};

typedef char const *env_t[ENV_SIZE];

static env_t env_names = {
    IRGEN_FILEIO_PRELOAD,
    IRGEN_CACHE_PATH,
    IRGEN_FILECACHE_SCRIPT,
    IRGEN_PATH_MIN_MATCH_PREFIX
};

static env_t envs = {
    0, // IRGEN_FILEIO_PRELOAD
    0, // IRGEN_CACHE_PATH
    0, // IRGEN_FILECACHE_SCRIPT
    0  // IRGEN_PATH_MIN_MATCH_PREFIX
};

static env_t unset_envs = {
    "LD_PRELOAD"
};

static int unset_envs_len = sizeof(unset_envs)/sizeof(unset_envs[0]);

static int env_init_stat = 0;

static char *open_modes[] = {
    "r",
    "w",
    "a",
    "r+",
    "w+",
    "a+"
};

static int open_flags[] = {
    O_RDONLY,
    O_WRONLY | O_CREAT | O_TRUNC,
    O_WRONLY | O_CREAT | O_APPEND,
    O_RDWR,
    O_RDWR | O_CREAT | O_TRUNC,
    O_RDWR | O_CREAT | O_APPEND
};
static size_t mode_len = sizeof(open_modes)/sizeof(open_modes[0]);

static void load_env(void) __attribute__((constructor));
static void unload_env(void) __attribute__ ((destructor));
static int load_fcache_env(env_t);
static void release_fcache_env(env_t);
static int is_relative_path(char const *);
static char *get_filepath_by_fd(int);
static char *get_filepath_by_fs(FILE *);
static void unset_runtime_env(char **);
static char **copy_str_array(char **);
static size_t size_of_array(char *const *);
static char ** construct_args(char *, char const*, char*);
static char *construct_cmd(char *, char const *, char const*);
static void free_str_array(char **);
static int check_filter_file(char const*, int);
static int get_flags_by_mode(char const *);


static void print_args(char *const * args){
    char *const *it = args;
    for(; it && (*it); ++it){
        printf("%s ", *it);
    }
    printf("\n");
}

// we should not use system but fork then with exec
#define FILTER_FORK_EXCE(filename, mode)\
    if (check_filter_file(filename, mode) == true){\
        unsetenv("LD_PRELOAD");\
        char *cmd = construct_cmd("python", envs[IRGEN_FILECACHE_SCRIPT_I], filename);\
        system(cmd);\
        free(cmd);\
        setenv("LD_PRELOAD", envs[IRGEN_FILEIO_PRELOAD_I], 1);\
    }


#ifdef HAVE_OPEN
int open(const char *path, int oflag, ...);
#endif

#ifdef HAVE_OPEN64
int open64(const char *pathname, int oflag, ...);
#endif

#ifdef HAVE_OPENAT
int openat(int fd, const char *path, int oflag, ...);
#endif

#ifdef HAVE_OPENAT64
int openat64(int fd, const char *path, int oflag, ...);
#endif

#ifdef HAVE_FOPEN
FILE *fopen(const char *restrict pathname, const char *restrict mode);
#endif

#ifdef HAVE_FOPEN64
FILE *fopen64 (const char *__restrict __filename, const char *__restrict __modes);
#endif

#ifdef HAVE_DLOPEN
void *dlopen(const char *filename, int flags);
#endif

#ifdef HAVE_DLMOPEN
void *dlmopen(Lmid_t lmid, const char *filename, int flags);
#endif

#ifdef HAVE_FDOPEN
FILE *fdopen(int fd, const char *mode);
#endif

#ifdef HAVE_FREOPEN
FILE *freopen(const char *restrict pathname, const char *restrict mode,
                     FILE *restrict stream);
#endif


#ifdef HAVE_OPEN
int open(const char *path, int oflag, ...){
    va_list mode;
    va_start(mode, oflag);
    int md = va_arg(mode, int);
    va_end(mode);
    typedef int (*func)(const char *, int, ...);
    DLSYM(func, fp, "open");
    int ret = fp(path, oflag, md);
    if (ret >= 0){
        char *filename = get_filepath_by_fd(ret);
        FILTER_FORK_EXCE(filename, oflag);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, oflag);
        free(filename);
    }
    return ret;
}
#endif

#ifdef HAVE_OPEN64
int open64(const char *pathname, int oflag, ...){
    va_list mode;
    va_start(mode, oflag);
    int md = va_arg(mode, int);
    va_end(mode);
    typedef int (*func)(const char *, int, ...);
    DLSYM(func, fp, "open64");
    int ret = fp(pathname, oflag, md);
    if (ret != -1){
        char *filename = get_filepath_by_fd(ret);
        FILTER_FORK_EXCE(filename, oflag);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, oflag);
        free(filename);
    }
    return ret;
}
#endif

#ifdef HAVE_OPENAT
int openat(int fd, const char *path, int oflag, ...){
    va_list args;
    va_start(args, oflag);
    int mode = va_arg(args, int);
    va_end(args);
    typedef int (*func)(int, const char *, int, ...);
    DLSYM(func, fp, "openat");
    int ret = fp(fd, path, oflag, mode);
    if (ret != -1){
        char *filename = get_filepath_by_fd(ret);
        FILTER_FORK_EXCE(filename, oflag);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, oflag);
        // printf("[INFO] Catched %s by %s, retuen code(%d)\n", path, __FUNCTION__, ret);
        free(filename);
    }
    return ret;
}
#endif

#ifdef HAVE_OPENAT64
int openat64(int fd, const char *path, int oflag, ...){
    va_list args;
    va_start(args, oflag);
    int mode = va_arg(args, int);
    va_end(args);
    typedef int (*func)(int, const char *, int, ...);
    DLSYM(func, fp, "openat64");
    int ret = fp(fd, path, oflag, mode);
    if (ret != -1){
        char *filename = get_filepath_by_fd(ret);
        FILTER_FORK_EXCE(filename, oflag);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, oflag);
        free(filename);
    }
    return ret;
}
#endif

#ifdef HAVE_FOPEN
FILE *fopen(const char *restrict pathname, const char *restrict mode){
    typedef FILE *(*func)(const char *restrict, const char *restrict);
    DLSYM(func, fp, "fopen");
    FILE *ret = fp(pathname, mode);
    if (ret){
        char *filename = get_filepath_by_fs(ret);
        int flags = get_flags_by_mode(mode);
        FILTER_FORK_EXCE(filename, flags);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s", filename, __FUNCTION__);
        free(filename);
    }
    return ret;
}
#endif

#ifdef HAVE_FOPEN64
FILE *fopen64 (const char *__restrict __filename, const char *__restrict __modes){
    typedef FILE *(*func)(const char *__restrict, const char *__restrict);
    DLSYM(func, fp, "fopen64");
    FILE *ret = fp(__filename, __modes);
    if (ret != NULL){
        char *filename = get_filepath_by_fs(ret);
        int flags = get_flags_by_mode(__modes);
        FILTER_FORK_EXCE(filename, flags);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, flags);
        free(filename);
    }
    return ret;
}
#endif

#ifdef HAVE_DLOPEN
void *dlopen(const char *filename, int flags){
    // only for share object
    typedef void *(*func)(const char *, int);
    DLSYM(func, fp, "dlopen");
    void *ret = fp(filename, flags);
    if (ret != NULL)
        FILTER_FORK_EXCE(filename, flags);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, flags);
    return ret;
}
#endif

#ifdef HAVE_DLMOPEN
void *dlmopen(Lmid_t lmid, const char *filename, int flags){
    // only for share object
    typedef void *(*func)(Lmid_t, const char *, int);
    DLSYM(func, fp, "dlmopen");
    void *ret = fp(lmid, filename, flags);
    if (ret != NULL)
        FILTER_FORK_EXCE(filename, flags);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, flags);
    return ret;
}
#endif

#ifdef HAVE_FDOPEN
FILE *fdopen(int fd, const char *mode){
    typedef FILE *(*func)(int, const char*);
    DLSYM(func, fp, __FUNCTION__);
    FILE *ret = fp(fd, mode);
    if (ret != NULL){
        char *filename = get_filepath_by_fs(ret);
        int flags = get_flags_by_mode(mode);
        FILTER_FORK_EXCE(filename, flags);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, flags);
        free(filename);
    }
    return ret;
}
#endif

#ifdef HAVE_FREOPEN
// most used in stream redirect
FILE *freopen(const char *restrict pathname, const char *restrict mode, FILE *restrict stream){
    typedef FILE *(*func)(const char *restrict, const char *restrict, FILE *restrict);
    DLSYM(func, fp, __FUNCTION__);
    FILE *ret = fp(pathname, mode, stream);
    if (ret != NULL){
        char *filename = get_filepath_by_fs(ret);
        int flags = get_flags_by_mode(mode);
        FILTER_FORK_EXCE(filename, flags);
        LOG(LOG_DEBUG, "[INFO] Catched %s by %s, with flags %d", filename, __FUNCTION__, flags);
        free(filename);
    }
    return ret;
}
#endif

static int is_relative_path(char const *path){
    if (path[0] == '/')
        return 0;
    return 1;
}

static char *get_filepath_by_fd(int fd){
    char *sylink = (char *)calloc(PATH_MAX+1, sizeof(char));
    if (snprintf(sylink, PATH_MAX+1, "%s/%d", FIELDESCRIPTORDIR, fd) < 0){
        perror("[ERROR] snprintf error");
        exit(EXIT_FAILURE);
    }
    char *filepath = (char *)calloc(PATH_MAX+1, sizeof(char));
    if (readlink(sylink, filepath, PATH_MAX) == -1){
        perror("[ERROR] readlink error!");
        exit(EXIT_FAILURE);
    }
    free(sylink);
    return filepath;
}

static char *get_filepath_by_fs(FILE *fs){
    int fd = fileno(fs);
    return get_filepath_by_fd(fd);
}

static char ** construct_args(char *python, char const *script, char *filepath){
    // hard code
    size_t argc = 3;
    char **args = (char **)calloc(argc + 1, sizeof(char *)); // python, script, filepath
    char **args_ptr = args;
    *(args_ptr++) = strdup(python);
    *(args_ptr++) = strdup(script);
    *(args_ptr++) = strdup(filepath);
    *args_ptr = NULL;
    return args; // need free
}

static char *construct_cmd(char *python, char const *script, char const*filepath){
    size_t len = strlen(python) + strlen(script) + strlen(filepath) + 3;
    char *cmd = (char *)calloc(len, sizeof(char));
    if (snprintf(cmd, len, "%s %s %s", python, script, filepath) < 0){
        perror("[ERROR] snprintf error!");
        exit(EXIT_FAILURE);
    }
    return cmd;
}

static void unset_runtime_env(char **environ){
    char **it = environ;
    for(int i=0; i<unset_envs_len; ++i){
        for(; (it) && (*it); ++it){
            if(0 == strncmp(unset_envs[i], *it, strlen(unset_envs[i]))){
                break;
            }

            if (it && (*it)){
                for(char **ii=it; (ii) && (*ii); ++ii){
                    *ii = *(ii + 1);
                }
            }
        }
    }
}

static size_t size_of_array(char *const *array){
    size_t cnt = 0;
    while(array[cnt] != NULL) ++cnt;
    return cnt;
}

static char **copy_str_array(char **in){
    size_t size = size_of_array(in);
    char **out = (char **)calloc(size+1, sizeof(char *));
    if (!out){
        perror("[ERROR] calloc error!");
        exit(EXIT_FAILURE);
    }
    char **out_ptr = out;
    for(char * const*it = in; (it) && (*it); ++ it, ++out_ptr){
        *out_ptr = strdup(*it);
        if (!(*out_ptr)){
            perror("[ERROR] strdup error!");
            exit(EXIT_FAILURE);
        }
    }
    out[size] = 0;

    return out;
}

static void free_str_array(char **array){
    char **it = array;
    for(; it && (*it); ++it){
        free((void *)(*it));
    }
    free(array);
}

static int check_filter_file(char const*__file, int flags){
    if (O_RDONLY != (flags & (O_RDWR | O_WRONLY)))
        return false;
    
    char const *min_match_prefix = envs[IRGEN_PATH_MIN_MATCH_PREFIX_I];
    if (0 == strncmp(min_match_prefix, __file, strlen(min_match_prefix))){
        return true;
    }
    return false;
}

static int get_flags_by_mode(char const *mode){
    // mode may contain 'b', such as "rb", we should strip 'b' first
    char *modes = strdup(mode);
    for(char *it = modes; (it) && (*it); ++it){
        if ((*it) == 'b'){
            for(char *ii = it; (ii) && (*ii); ++ii){
                *ii = *(ii + 1);
            }
        }
    }
    

    int flags = 0;
    for(int i=0; i<mode_len; ++i){
        if (0 == strcmp(open_modes[i], modes)){
            flags = open_flags[i];
        }
    }
    free(modes);
    return flags;
}


static int load_fcache_env(env_t env_list){
    int status = 0;
    for (int i; i<ENV_SIZE; ++i){
        char const *const env_value = getenv(env_names[i]);
        char const *const env_copy = env_value ? strdup(env_value) : env_value;
        env_list[i] = env_copy;
        status = (env_copy ? 1 : 0);
    }
    return status;
}

static void release_fcache_env(env_t env_list){
    for (int i; i<ENV_SIZE; ++i){
        free((void *)(env_list[i]));
    }
}

static void load_env(void){
    if (env_init_stat == false)
        env_init_stat = load_fcache_env(envs);
    
    if (env_init_stat == false)
        exit(EXIT_FAILURE);
}

static void unload_env(void){
    release_fcache_env(envs);
}